feat: add vouch and report API endpoints with NIP-98 auth#7
Open
CodyTseng wants to merge 10 commits into
Open
Conversation
Two new POST endpoints (gated by vouch.enabled config) let users submit signed claims directly instead of through a Nostr event. Vouches act as equal-weight follow edges in the ranking graph, deduped against any existing follow from the same source. Reports apply a trust-weighted penalty to the target's final score: final = raw * (1 - R/(R+F)). POST /vouch and POST /report are mutually exclusive per (source, target): posting one atomically removes the opposite side, so there is no DELETE endpoint — toggling sides is the only way to retract. Submissions from pubkeys with no TrustRank (and not in seed_pubkeys) return 200 but are silently dropped to prevent spam-account inflation. The API now opens the DB in read-write mode; WAL + writeMu already coordinate it with the crawler. Migration v4 adds vouches and reports tables. 26 new tests cover NIP-98 middleware, repository mutex/toggle behaviour, and ranking integration (vouch promotes unfollowed users, reports decay scores, untrusted reporters are ignored, follow+vouch edges dedupe). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both API and crawler now open the database read-write, so the ModeReadOnly branch was dead code. Collapse to a single New(path) entry point. The API previously inherited the write-mode pool size of 1, which would have serialized its reads; restore a 10-connection pool for all callers. Writes are still serialized by writeMu and SQLite's own locks, so extra connections do not cause contention. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A vouch is a weaker signal than an actual follow: the user is asserting non-spam without committing to see the target's posts. Carry a per-edge weight through the graph so vouch-only edges contribute proportionally less flow than a full follow. Config: vouch.weight (default 0.5, range (0, 1]). PageRank and TrustRank inner loops now divide by outWeight (sum of outgoing edge weights) instead of outDegree (count). Each in-edge carries its own weight; a source's score flows to each target in proportion to weight / outWeight. Follow and vouch edges share the same adjacency list — only their weights differ. outDegree is still tracked as an int so the pubkeys table's Following column remains a count. New test verifies that lowering the weight produces a lower score for a vouch-only recipient. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Having both fields required the user to keep them consistent: enabling with weight=0 (or disabling with weight=0.5) both produced inconsistent states. Use weight alone — 0 means off (endpoints return 404 and the ranking calculator skips streaming vouches entirely), > 0 means on and is the edge weight. Default 0 preserves prior off-by-default behaviour. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vouches and reports are now plain signed Nostr events rather than a NIP-98-wrapped private API, so the data is publishable to any relay and shared with the wider network. A new internal/ingest package backs both ingestion paths: - Crawler (pull): alongside kind:3/0, fetches each author's kind:1984 reports and kind:10040 vouch sets. Replaceable kinds (3/0/10040) share one small-limit query; kind:1984 (append-only) gets its own query capped at the newest 50 so it can't crowd out the replaceable events. - POST /event (push): accepts a single signed event for immediate ingestion, keeping the anti-inflation rule (untrusted authors dropped). Both paths verify signatures before storing. The old /vouch and /report endpoints and the NIP-98 middleware are retired. Reports are kind:1984 profile-level (p tag, no e tag) spam/impersonation events. Vouches are membership in a custom replaceable kind:10040 set and follow the same lifecycle as follow edges: refreshed via last_seen, never actively deleted, aged out by the ranking staleness window. Vouch-beats- report precedence is resolved at ranking time (GetTrustWeightedReports excludes a reporter who also vouches for the same target).
A source that both vouches for and reports the same target no longer gets special handling. The vouch adds flow and the report subtracts it at ranking time, which roughly cancels out on its own — so the extra NOT EXISTS subquery in GetTrustWeightedReports was redundant complexity.
Vouches now live in a standard NIP-51 follow set (kind:30000) tagged d=vouch, instead of a custom kind:10040. Other clients can render it as a people list, and the generic identifier (no project prefix) leaves room for a shared convention. Other follow sets are ignored. Because kind:30000 is addressable by d, it can't share the no-d query with kind:3/0; it gets its own #d-filtered fetch (fetchVouchSetsFromRelay), and ingest gates it via IsVouchSet.
A REQ carries multiple filters (OR'd), so the kind:3/0 filter and the kind:30000 vouch-set filter (constrained by #d, scoped to its own filter) now ride in a single SubManyEose call per relay — one connection, one round-trip — instead of two separate fetches. kind:1984 reports stay in their own subscription to keep their per-query limit clean.
- ingest: drop hand-written dTagValue, use go-nostr's Tags.GetD() - ranking: compute report-penalty F only for reported targets instead of scanning the whole graph (drops an O(numNodes) alloc + full edge scan) - repository/models: remove unused ReportAggregate.NumReporters field and its COUNT(*) (only TotalReporterTrust is consumed)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
POST /vouchandPOST /reportendpoints (NIP-98 authenticated, gated byvouch.enabled) let users contribute to the reputation graph without actually following each other — solving the "I know this new account isn't spam but I don't want to follow them" cold-start problem.(source, target)pair. There are no DELETE endpoints — reversing a stance requires posting the opposite stance.final = raw * (1 - R / (R + F))where R is the summed trust of trusted reporters and F is the summed trust of the target's incoming edges.seed_pubkeysreturn 200 but are not persisted (prevents Sybil inflation while keeping the client unable to probe the admission oracle).ModeReadWrite; WAL mode + the existingwriteMualready serialise writes safely across the crawler and API processes. Migration v4 adds thevouchesandreportstables.Test plan
go test ./...— 26 new tests pass (11 repository, 11 NIP-98 middleware, 4 ranking)go vet ./...cleango build ./cmd/apiandgo build ./cmd/crawlerproduce binariesvouch.enabled: true🤖 Generated with Claude Code